iT邦幫忙

2024 iThome 鐵人賽

DAY 4
1

https://ithelp.ithome.com.tw/upload/images/20240918/20168201HHuDEbSJ00.png

Revealing Module (闡明模組)模式其實可算是 Module 模式的進階版本,Revealing Module 是由 Christian Heilmann 所提出的,接下來就來看看此模式的問題與解決方案。

情境

在複雜應用程式中,我們需要同時管理私有狀態和函式,並揭露外部可使用、存取的方法,以避免全域污染,又同時提高程式碼的可重用性。

問題

如何在不暴露模組內部所有細節的情況下,允許外部訪問模組的特定功能或變數?在沒有封裝機制的情況下,模組內的變數和函式都是公開的,這可能導致不安全或不可預測的程式碼行為。

權衡

  • 封裝性:須保持模組內部變數和函式的隱私
  • 可訪問性:外部環境需要存取模組中的特定功能
  • 維護性:容易維護和擴展的程式碼結構是開發大型應用時的重要因素
  • 可重用性:模組應該是可重用的,而不依賴於全局狀態

解法

此問題解法是 Revealing Module,在介紹 Revealing Module 模式之前,不知道大家有沒有跟我一樣的困惑,覺得上述的問題用我們昨天提過的 Module 模式就可以解決了啊? 在此之前,想先稍微介紹一下,在 ES6 模組功能出現以前,JavaScript 是如何管理私有和公共變數與方法的,因 JavaScript 沒有 privatepublic 語法,因此是透過 closure 的方式將函式與變數限制在特定範圍內,將私有變數限制在函式作用域內,將外部可存取的 return 出去,一般 Module 用 IIFE 的範例如下:

const myGeneralModule = (function () {
  // 私有變數
  const privateVariable = 'I am private';

  return {
    // 公共方法
    publicMethod: function () {
      console.log(privateVariable);
    },
    publicHello: function () {
      console.log('hello')
    }
  };
})();

myGeneralModule.publicMethod(); // I am private
myGeneralModule.publicHello(); // hello
myGeneralModule.privateVariable; // undefined,無法存取私有變數

可以看到我們透過立即執行函式來建立函式作用域,將私有變數包在函式內,只拿到 return 的值並依此執行公有方法,且無法存取到私有變數,但一般 Module 有個小問題,就是我們需要在 return 的時候才會宣告要揭露的公有函式內容,會讓整個結構稍微凌亂些。
而 Revealing Module 對應上述 Module 範例,會將所有變數和函式先定義好,在最後 return 時才會指定要揭露哪些變數和函式,我們可以依照以下步驟來實現 Revealing Module:

  1. 創建立即執行的函式(IIFE):利用 IIFE 建立閉包,函式內包含模組的所有變數和函式
  2. 定義所有變數和函式:在函式內定義此模組功能需要的所有變數和函式
  3. 回傳公開變數和方法:在 IIFE 底部回傳一個物件,此物件包含要公開、可供外部存取的方法和屬性,這裡會控制模組要揭露的部分,也可以為這些方法再另外取名

Revealing Module 程式碼範例如下:

const myRevealingModule = (function () {
  // 私有變數
  const privateVariable = 'I am private';
  
  const publicMethod = function () {
     console.log(privateVariable);
    };
    
  const publicHello = function () {
      console.log('hello')
    }
    
  // 在最後回傳時決定要揭露的方法,也可另外再取其他名稱
  return {
    printVariable: publicMethod,
    printHello: publicHello
  };
})();

myRevealingModule.printVariable(); // I am private
myRevealingModule.printHello(); // hello

簡單來說,Revealing Module 會在程式碼最後再決定要揭露的變數和函式,如果要看模組的公開接口有哪些,直接看程式碼尾端即可,讓整體程式碼更一致、好維護也好理解,不過上述都還在 IIFE 階段,但我們現在有了 ES6 的 importexport 語法啦! 在 ES6 模組中,我們不需要再用 return 什麼來區分公有和私有變數,而是只要有 export 才算公有、沒有 export 一律視為私有,因此 Revealing Module 應用在 ES6 模組寫法,其實就是把要匯出的東西統一在程式碼最下面一起export

let counter = 0;
let userName = 'Foo';

const publicAddCount = () => {
    counter++
};

const publicGetUserName = () => {
    console.log(`user name is ${userName}`);
};

const publicSetUserName = (newUserName) => {
    userName = newUserName;
};

// 最後統一匯出外部可存取的方法,也可更改名稱
const myRevealingModule = {
    addCount: publicAddCount,
    getUserName: publicGetUserName,
    setUserName: publicSetUserName
}

export default myRevealingModule

在其他檔案就可以這樣使用:

import myRevealingModule from './myRevealingModule';

myRevealingModule.getUserName();

優點

以 Revealing Module 作為解決方案的優點如下:

  • 程式碼語法一致
  • 在模組尾端統一定義公有變數和函式,讓人較易理解哪些函式和變數可供存取,提高可讀性

缺點

以 Revealing Module 作為解決方案的缺點如下:

  • 有部分重複性的程式碼,因為在最後才會定義匯出的方法,會需要重複提及變數名稱,而不是像一般模組可以直接在需要匯出的變數前面加 export 就可匯出了

其他缺點我覺得昨天提到的,一般的 Module 模式也有類似問題,但也列上來~

  • 修改公有方法的困難
    • 當私有函式參照到公有函式時,如果之後需要修改或替換公有函式會遇到問題,即使在模組外部修改公有函式,模組內部的函式還是會引用原來的公有函式,因為私有函式在創建時就已經綁定了對公共函式的引用,這個綁定不會因為外部改變而更新。簡要一句話來說,就是:從外部修改公有函式,無法改到模組內該函式的內容,一種治標不治本的感覺~
  • 無法修補的公有成員/變數
    • 公有物件的成員參照私有變數時也有上一點的類似問題,公有物件成員(如方法或屬性)如果直接使用私有變數,即使在模組外部對這些公有成員修改,私有變數的引用仍保持不變,也就是說,在不觸及私有作用域的情況下,會限制更新和修補這些公有成員的能力,若應用程式複雜,會導致維護困難

其他補充

以我自己目前撰寫模組的習慣,比較少聚集在最後一起 export,通常都是定義變數和函式時,直接在需要的地方前面加個 export 語法就匯出了,不過這樣的確也會讓程式碼比較零散,有的有 export 有的沒有,也會比較難看出這到底有沒有 export 讓外部使用,因此若對程式碼結構一致性或可讀性有要求,或許可考慮 Revealing Module 的方式。

Reference


上一篇
[Day 03] Module 模式
下一篇
[Day 05] Singleton 模式
系列文
30天的 JavaScript 設計模式之旅30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言